# param_funs.py

r'''This has the parameter functions.
'''

from __future__ import with_statement, division
import re
import sys   # for debugging
import math

No_default = object()  # a globally unique value

class param_ref(object):
    r'''References another parameter.

    ``param_name`` must be <component_name>.<param_name> or <param_name>.
    '''
    def __init__(self, param_name, default = No_default):
        names = param_name.split('.')
        if len(names) == 2:
            self.component_name, self.param_name = names
        elif len(names) != 1:
            raise ValueError("param_ref: illegal param_name, {0!r}"
                               .format(param_name))
        else:
            self.component_name = None
            self.param_name = names[0]
        self.default = default

    def __call__(self, params):
        return self.get(params)

    def get(self, params):
        if self.component_name is None:
            if self.param_name in params:
                return get_param(params, self.param_name)
            if self.default is not No_default:
                return self.default
            raise KeyError("{0!r} not set as a parameter"
                             .format(self.param_name))
        return get_param(params, self.component_name) \
                 .get_param(params, self.param_name, self.default)

class param_fun(object):
    r'''Function object for parameter functions.

    Call ``fn`` with ``args`` looked up in params.

    Each arg in args is either a string of the form
    <component_name>.<param_name> or a 2-tuple or a 3-tuple:

    - (component_name, param_name)
    - (component_name, param_name, default)
    '''
    def __init__(self, fn, *args):
        self.fn = fn
        self.args = tuple(tuple(arg.split('.')) if isinstance(arg, str)
                                                else arg
                          for arg in args)

    def __call__(self, params):
        return self.get(params)

    def get(self, params):
        return self.fn(*tuple((get_param(params, arg[0])
                                 .get_param(params, *arg[1:])
                               if len(arg) > 1
                               else get_param(params, arg[0]))
                              for arg in self.args))

Var_re = re.compile(r'\$.\w*(?:\.\w+)?')

class param_eval(object):
    r'''Eval text as a parameter function.

    Call ``eval_text`` with all $vars looked up in params.

        >>> pe = param_eval('$foo')
        >>> pe.get({'foo': 44, 'bar': 55})
        44
        >>> pe2 = param_eval('$foo + $bar')
        >>> pe2.get({'foo': 44, 'bar': 55})
        99
    '''
    def __init__(self, eval_text):
        param_names = set()
        args = {}   # {arg: param_name}
        m = Var_re.search(eval_text)
        while m:
            arg = m.group(0)[1:]
            if arg in args:
                param_name = args[arg]
            else:
                param_name = arg.split('.')[-1]
                sym_num = 2
                suffix = ''
                while param_name + suffix in param_names:
                    suffix = '_' + str(sym_num)
                    sym_num += 1
                param_name += suffix
                args[arg] = param_name
            eval_text = \
              eval_text[:m.start(0)] + param_name + eval_text[m.end(0):]
            m = Var_re.search(eval_text)
        self.args = args
        self.eval_text = eval_text

    def __call__(self, params):
        return self.get(params)

    def get(self, params):
        local_vars = {}
        for key, param_name in self.args.iteritems():
            value = None
            for name in key.split('.'):
                if value is None: value = get_param(params, name)
                else: value = value.get_param(params, name)
            local_vars[param_name] = value
        return eval(self.eval_text, globals(), local_vars)

class fun_value(object):
    r'''This turns a function into a parameter value.

    Normally, the parameter lookup mechanism wants to call functions.  This
    wrapper prevents that from happening.
    '''
    def __init__(self, fn):
        self.fn = fn

    def __call__(self, params):
        #print >> sys.stderr, "fun_value returning", self.fn
        return self.fn

def get_param(params, name):
    try:
        return get_value(params[name], params)
    except KeyError:
        print >> sys.stderr, "params"
        for key in sorted(params.iterkeys()):
            print >> sys.stderr, "  ", key, params[key]
        raise

def get_value(v, params):
    if hasattr(v, '__call__'): return v(params)
    return v

